package bancosys.tec.services.implementor.descriptor;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;

import jmine.tec.di.metadata.DependencyMetaData;
import jmine.tec.services.annotations.Execution;
import jmine.tec.services.annotations.ServiceImplementor;
import jmine.tec.utils.reflection.fast.FastMethodFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import bancosys.tec.services.implementor.annotations.visitor.DependencyMetadataParser;
import bancosys.tec.services.implementor.execution.ServiceExecutionProxyImpl;

/**
 * Cria instancias de {@link ServiceExecutionProxy}.
 */
public class ExecutionProxyDescriber {

    private final DependencyMetadataParser dependencyMetadataParser;

    private final FastMethodFactory methodFactory;

    /**
     * C'tor
     * 
     * @param dependencyMetadataParser o parser de annotations
     * @param methodFactory o {@link FastMethodFactory}
     */
    public ExecutionProxyDescriber(DependencyMetadataParser dependencyMetadataParser, FastMethodFactory methodFactory) {
        super();
        this.dependencyMetadataParser = dependencyMetadataParser;
        this.methodFactory = methodFactory;
    }

    /**
     * Cria um {@link ServiceExecutionProxy} para o tipo passado.
     * 
     * @param type o tipo.
     * @return {@link ServiceExecutionProxy}
     */
    public ServiceExecutionProxy createExecutionProxyFor(Class<?> type) {
        Method executionMethod = this.findExecutionMethod(type);
        List<? extends DependencyMetaData> dependencies =
                this.dependencyMetadataParser.getClassReader().visitMethodParametersAnnotation(executionMethod, type);
        DependencyMetaData[] deps = dependencies.toArray(new DependencyMetaData[dependencies.size()]);
        return new ServiceExecutionProxyImpl(this.methodFactory.getMethodAccessorFor(executionMethod), deps);
    }

    /**
     * Procura o metodo que executa o servico
     * 
     * @param type o tipo
     * @return {@link Method}
     */
    private Method findExecutionMethod(Class<?> type) {
        Method exec = this.findExecutionMethodRecurse(type);
        Class<?> annotated = AbstractServiceAnnotationParser.findTypeWithAnnotation(type);
        final ServiceImplementor service = annotated.getAnnotation(ServiceImplementor.class);
        if (service.executeMethodName().length() > 0) {
            Method withName = this.findExecutionMethodWithName(type, service.executeMethodName());
            if (withName != null) {
                if (exec != null) {
                    this.getLogger().info("Execution method " + withName + " overriden");
                } else {
                    return withName;
                }
            }
        }
        if (exec == null) {
            throw new IllegalArgumentException("Cannot find execution method on: " + type);
        }
        return exec;
    }

    /**
     * @return {@link Log}
     */
    private Log getLogger() {
        return LogFactory.getLog(this.getClass());
    }

    /**
     * Procura o metodo 'executável' com o nome passado
     * 
     * @param type o tipo
     * @param executeMethodName o nome do metodo
     * @return o {@link Method}
     */
    private Method findExecutionMethodWithName(Class<?> type, String executeMethodName) {
        Method[] methods = type.getMethods();
        for (Method method : methods) {
            if (method.getName().equals(executeMethodName)
                    && AbstractServiceAnnotationParser.isValidInjectionParameter(method.getParameterAnnotations())) {
                return method;
            }
        }
        return null;
    }

    /**
     * Procura metodos marcados com a anotacao {@link Execution}, subindo para superclasses se necessario. Devolve null se nada for
     * encontrado.
     * 
     * @param type o tipo
     * @return {@link Method}
     */
    private Method findExecutionMethodRecurse(Class<?> type) {
        if (type == null || Object.class == type) {
            // resolve by annotation
            return null;
        } else {
            Method exec = this.findExecutionMethodOn(type);
            if (exec != null) {
                if (Modifier.isStatic(exec.getModifiers())) {
                    throw new IllegalArgumentException("cannot put @Execution on static methods");
                }
                Method[] overriden = type.getMethods();
                for (Method method : overriden) {
                    if (method.isAnnotationPresent(Execution.class) && method != exec) {
                        this.getLogger().info("Execution method " + method + " overriden");
                    }
                }
                return exec;
            }
            return this.findExecutionMethodRecurse(type.getSuperclass());
        }
    }

    /**
     * Devolve o metodo marcado com a anotacao {@link Execution} declarado no tipo passado. Devolve null se nada for achado, mas lanca uma
     * excecao se mais de um metodo for encontrado declarado no tipo passado.
     * 
     * @param type o tipo
     * @return {@link Method}
     */
    private Method findExecutionMethodOn(Class<?> type) {
        Method[] methods = type.getDeclaredMethods();
        Method exec = null;
        for (Method method : methods) {
            if (Modifier.isPublic(method.getModifiers()) && method.isAnnotationPresent(Execution.class)) {
                if (exec == null) {
                    exec = method;
                } else {
                    throw new IllegalArgumentException("cannot define two execution methods on the same type");
                }
            }
        }
        return exec;
    }

}
